// Copyright (c) Open Enclave SDK contributors.
// Licensed under the MIT License.

#include "ecall_ids.h"
#include <openenclave/internal/raise.h>
#include <stdlib.h>
#include "hostthread.h"

// Initial size of ecall table mapping ecall names to global id.
// Most enclaves in OE SDK repo have fewer than 16 ecalls.
#define OE_ECALL_TABLE_INITIAL_SIZE 16

/* The maximum numer of different ecalls that an application can make is
 * bounded. */
static const char** _ecall_table;
static uint32_t _ecall_table_capacity;
static uint32_t _ecall_table_size;

/* Mutex for assigning/looking up global ids in a thread-safe manner. */
static oe_mutex _lock = OE_H_MUTEX_INITIALIZER;

/* Cleanup memory during program terminaton */
static void _free_ecall_table(void)
{
    oe_free((void*)_ecall_table);
}

/* Get the global ecall id from the _ecall_table. Locking must be done by
 * caller. */
static oe_result_t _get_global_id(const char* name, uint64_t* global_id)
{
    oe_result_t result = OE_NOT_FOUND;
    if (!name || !global_id)
        OE_RAISE(OE_INVALID_PARAMETER);

    /* Search for id assigned for given name. */
    for (uint64_t i = 0; i < _ecall_table_size; i++)
    {
        if (strcmp(_ecall_table[i], name) == 0)
        {
            *global_id = i;
            break;
        }
    }

    /* If the name is not found, adding it to the table. */
    if (*global_id == OE_GLOBAL_ECALL_ID_NULL)
    {
        // Resize table if needed.
        if (_ecall_table_size == _ecall_table_capacity)
        {
            if (!_ecall_table)
            {
                _ecall_table_capacity = OE_ECALL_TABLE_INITIAL_SIZE;
                atexit(_free_ecall_table);
            }
            else
            {
                _ecall_table_capacity *= 2;
            }
            _ecall_table = oe_realloc(
                (void*)_ecall_table, _ecall_table_capacity * sizeof(char*));
            if (!_ecall_table)
                OE_RAISE(OE_OUT_OF_MEMORY);
        }

        _ecall_table[_ecall_table_size] = name;
        *global_id = _ecall_table_size;
        _ecall_table_size++;
    }

    result = OE_OK;
done:
    return result;
}

oe_result_t oe_get_global_id(const char* name, uint64_t* global_id)
{
    oe_result_t result = OE_UNEXPECTED;
    bool locked = false;
    if (oe_mutex_lock(&_lock) != 0)
        OE_RAISE(OE_FAILURE);
    locked = true;

    result = _get_global_id(name, global_id);
done:
    if (locked)
        oe_mutex_unlock(&_lock);
    return result;
}

oe_result_t oe_get_ecall_ids(
    oe_enclave_t* enclave,
    const char* name,
    uint64_t* global_id,
    uint64_t* id)
{
    oe_result_t result = OE_FAILURE;
    oe_ecall_id_t* ecall_id_table = NULL;
    uint64_t ecall_id_table_size = 0;

    /* Validate parameters */
    if (!enclave || !global_id || !name || !id)
    {
        result = OE_INVALID_PARAMETER;
        goto done;
    }

    /* Initialize output parameter to NULL id */
    *id = OE_ECALL_ID_NULL;

    /* Fetch ecall_id_table using platform agnostic API */
    OE_CHECK(
        oe_get_ecall_id_table(enclave, &ecall_id_table, &ecall_id_table_size));

    /* Lookup/assign global id */
    if (*global_id == OE_GLOBAL_ECALL_ID_NULL)
    {
        /* The global_id variable is generated by edger8r, one global id
         *  variable per ecall. The same variable will be reused every time that
         * ecall is made. Therefore, for an ecall, the following code will be
         * executed only once.
         */
        OE_CHECK(oe_get_global_id(name, global_id));
    }

    /* Look up function id based on global id. */
    if (*global_id >= ecall_id_table_size)
        OE_RAISE(OE_NOT_FOUND);

    /* Look-up the ecall id from the per-enclave table.
     * The ecall_id_table is expected to be set up during enclave creation
     */
    *id = ecall_id_table[*global_id].id;
    if (*id == OE_ECALL_ID_NULL)
        OE_RAISE(OE_NOT_FOUND);

    result = OE_OK;
done:
    return result;
}

oe_result_t oe_register_ecalls(
    oe_enclave_t* enclave,
    const oe_ecall_info_t* ecall_info_table,
    uint32_t num_ecalls)
{
    oe_result_t result = OE_UNEXPECTED;
    oe_ecall_id_t* ecall_id_table = NULL;
    uint64_t max_global_id = 0;
    uint64_t ecall_id_table_size = 0;
    bool locked = false;

    /* Validate parameters */
    if (!enclave || !ecall_info_table || !num_ecalls)
        OE_RAISE(OE_INVALID_PARAMETER);

    if (oe_mutex_lock(&_lock) != 0)
        OE_RAISE(OE_FAILURE);
    locked = true;

    /* Iterate through the ecalls and assign global ids.
     * Also find out the maximum global id for the enclave. */
    for (uint32_t i = 0; i < num_ecalls; i++)
    {
        uint64_t global_id = OE_GLOBAL_ECALL_ID_NULL;
        const char* name = ecall_info_table[i].name;

        /* Assign a proper global id based on the global __ecall_table. */
        OE_CHECK(_get_global_id(name, &global_id));
        if (global_id > max_global_id)
            max_global_id = global_id;
    }

    /* Allocate ecall id table for the enclave */
    ecall_id_table_size = max_global_id + 1;
    ecall_id_table =
        (oe_ecall_id_t*)oe_malloc(sizeof(uint64_t) * ecall_id_table_size);
    if (!ecall_id_table)
        OE_RAISE(OE_OUT_OF_MEMORY);

    for (uint64_t i = 0; i < ecall_id_table_size; ++i)
        ecall_id_table[i].id = OE_ECALL_ID_NULL;

    /* Fill the ecall id table */
    for (uint32_t i = 0; i < num_ecalls; i++)
    {
        uint64_t global_id = OE_GLOBAL_ECALL_ID_NULL;
        const char* name = ecall_info_table[i].name;
        uint64_t local_id = i;

        OE_CHECK(_get_global_id(name, &global_id));
        ecall_id_table[global_id].id = local_id;
    }

    OE_CHECK(
        oe_set_ecall_id_table(enclave, ecall_id_table, ecall_id_table_size));

    result = OE_OK;

done:
    if (locked)
        oe_mutex_unlock(&_lock);

    return result;
}
